package com.hzgj.bcl.soa.ws;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.hzgj.bcl.soa.stats.StatsData;
import com.hzgj.bcl.soa.util.Triple;
import com.hzgj.bcl.util.lang.StrKit;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by Administrator on 2016/9/30.
 */
@Component
public class ServiceRelation {
    private Logger logger = LoggerFactory.getLogger(getClass());
    //correlateId:service_method:(method,ip:port,span)
    Cache<String, Map<String, Triple<String, String, Long>>> correlateMapping;
    Cache<String, Node> cacheNode = CacheBuilder.newBuilder().expireAfterWrite(3, TimeUnit.MINUTES).build();
    Map<String, List<Node>> need2Mapping = Maps.newHashMap();
    AtomicInteger count = new AtomicInteger(0);
    //chainId:(serviceName_method)
    Map<String, Set<String>> serviceGroup = Maps.newHashMap();
    //service_method:chainIds
    Map<String, Set<String>> serviceAndMethod2ChainMapping = Maps.newHashMap();
    //chainId,contextId
    Cache<String, String> chain2CorrelateId = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();


    public void clear() {
        serviceGroup.clear();
        serviceAndMethod2ChainMapping.clear();
    }

    public ServiceRelation() {
        correlateMapping = CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.MINUTES).
                removalListener(notification -> {
                    Map<String, Triple<String, String, Long>> value = (Map<String, Triple<String, String, Long>>) notification.getValue();
                    String key = null;
                    Set<String> serviceAndMethods = value.keySet();
                    if (!serviceGroup.containsValue(serviceAndMethods)) {
                        String _key = "chain" + count.getAndIncrement();
                        serviceGroup.put(_key, serviceAndMethods);
                        key = _key;
                    } else {
                        Optional<Map.Entry<String, Set<String>>> optional = serviceGroup.entrySet().stream().
                                filter(entry -> CollectionUtils.isEqualCollection(entry.getValue(), serviceAndMethods)).findFirst();
                        if (optional.isPresent()) {
                            key = optional.get().getKey();
                        }
                    }
                    if (StrKit.isBlank(key)) {
                        return;
                    }
                    String finalKey = key;
                    for (String entry : serviceAndMethods) {
                        serviceAndMethod2ChainMapping.compute(entry, (s, keys) -> {
                            if (keys == null) {
                                keys = Sets.newHashSet();
                            }
                            keys.add(finalKey);
                            return keys;
                        });
                    }
                }).build();
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(30);
                    List<Set<String>> nodes = Lists.newArrayList(serviceGroup.values());
                    nodes.sort((o1, o2) -> {
                        if (o1.size() > o2.size()) {
                            return -1;
                        } else if (o1.size() < o2.size()) {
                            return 1;
                        } else {
                            return 0;
                        }
                    });
                    Set<Set<String>> removed = Sets.newHashSet();
                    for (int i = 0; i < nodes.size(); i++) {
                        for (int j = i + 1; j < nodes.size(); j++) {
                            if (CollectionUtils.containsAll(nodes.get(i), nodes.get(j))) {
                                removed.add(nodes.get(j));
                            }
                        }
                    }
                    if (removed.size() > 0) {
                        Map<Set<String>, String> newGroup = MapUtils.invertMap(serviceGroup);

                        Set<String> removedChainIds = Sets.newHashSet();
                        removed.forEach(i -> {
                            String chainId = newGroup.get(i);
                            if (StrKit.notBlank(chainId)) {
                                removedChainIds.add(chainId);
                                serviceGroup.remove(chainId);
                            }
                        });
                        serviceAndMethod2ChainMapping.entrySet().forEach(entry -> {
                            removedChainIds.stream().filter(chainId -> entry.getValue().contains(chainId)).forEach(chainId -> {
                                entry.getValue().remove(chainId);
                            });
                        });
                    }
                } catch (InterruptedException e) {

                }
            }
        }).start();
    }

    public Node getNode(final String name) {
        return cacheNode.getIfPresent(name);
    }

    public Collection<String> getMethod(String service) {
        Node node = cacheNode.getIfPresent(service);
        if (node != null) {
            return node.methods;
        }
        return Collections.emptyList();
    }

    /**
     * @param serviceAndMethod
     * @return serviceName:(method,ip:port,span)
     */
    public Map<String, Triple<String, String, Long>> getInvokeTimeByChain(String serviceAndMethod) {
        Map<String, Triple<String, String, Long>> result = Maps.newHashMap();
        Set<String> chainIds = serviceAndMethod2ChainMapping.get(serviceAndMethod);
        if (chainIds != null) {
            for (String chainId : chainIds) {
                String correlateId = chain2CorrelateId.getIfPresent(chainId);
                if (StrKit.notBlank(correlateId)) {
                    Map<String, Triple<String, String, Long>> chainInfo = correlateMapping.getIfPresent(correlateId);
                    if (chainInfo != null) {
                        result.putAll(chainInfo);
                    }
                }
            }
        }
        return result;
    }

    //[{"id":1,"text":"Root node","children":[{"id":2,"text":"Child node 1"},{"id":3,"text":"Child node 2"}]}]
    public Object getJstree() {
        List<Child> children = Lists.newArrayList();
        Root root = new Root("root", "服务", new State(), children);
        cacheNode.asMap().keySet().forEach(name -> {
            children.add(new Child(name, name));
        });
        return root;
    }

    @AllArgsConstructor
    @Getter
    static class Root {
        String id;
        String text;
        State state;
        List<Child> children;
    }

    @Getter
    static class State {
        boolean opened = true;
    }


    @AllArgsConstructor
    @Getter
    static class Child {
        String id;
        String text;
    }


    public void build(List<StatsData> cols) {
        for (StatsData statsData : cols) {
            String contextId = statsData.contextId;
            if (StrKit.isBlank(contextId)) {
                continue;
            }
            try {
                Node node = cacheNode.get(statsData.service, () -> getNode(statsData));
                node.methods.add(statsData.method);
                String parentId = statsData.parentId;
                if (StrKit.notBlank(parentId)) {
                    int split = parentId.indexOf("_");
                    if (split < 0) {
                        continue;
                    }
                    String pid = parentId.substring(0, split);
                    Node pnode = cacheNode.getIfPresent(pid);
                    if (pnode != null) {
                        pnode.outs.add(node);
                        node.ins.add(pnode);
                    } else {
                        need2Mapping.computeIfAbsent(pid, s -> Lists.newArrayList()).add(node);
                    }
                }

                Triple<String, String, Long> triple = new Triple<>(statsData.method, statsData.dstHost + ":" + statsData.dstPort, statsData.span);
                Map<String, Triple<String, String, Long>> correlate_services = correlateMapping.get(contextId, () -> Maps.newHashMap());
                correlate_services.put(statsData.service + "_" + statsData.method, triple);

                String chainId = getChainId(correlate_services.keySet());
                if (StrKit.notBlank(chainId)) {
                    chain2CorrelateId.put(chainId, contextId);
                }

                List<Node> needMapping = need2Mapping.get(statsData.service);
                if (needMapping != null) {
                    needMapping.forEach(need -> {
                        need.ins.add(node);
                        node.outs.add(need);
                    });
                    needMapping.remove(statsData.service);
                }
            } catch (ExecutionException e) {
                logger.error("{}", e);
            }
        }
    }

    Node getNode(StatsData statsData) {
        Node _node = new Node();
        _node.name = statsData.service;
        _node.methods.add(statsData.method);
        return _node;
    }


    String getChainId(Set<String> serviceAndMethods) {
        for (Map.Entry<String, Set<String>> entry : serviceGroup.entrySet()) {
            if (CollectionUtils.isEqualCollection(entry.getValue(), serviceAndMethods)) {
                return entry.getKey();
            }
        }
        return null;
    }


    @Getter
    public static class Node {
        String name;

        @Override
        public String toString() {
            final StringBuffer sb = new StringBuffer("Node{");
            sb.append("name='").append(name).append('\'');
            sb.append('}');
            return sb.toString();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Node node = (Node) o;
            return Objects.equals(name, node.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name);
        }

        Set<String> methods = Sets.newHashSet();
        Set<Node> ins = Sets.newHashSet();
        Set<Node> outs = Sets.newHashSet();
    }
}
